<?php

namespace Movie;

use Selector;
use IFileCache;
use BadRequestException;

/**
 * Class AbstractMovie
 * @package Movie
 * User:大卫
 * Mail:367013672@qq.com
 * Time:2020-7-1 12:33:40
 * Desc:从第三方影视站拉取电影信息，设置缓存：1级.入口参数级（按需）；2级.文件缓存（大）
 */
abstract class AbstractMovie
{
    // 调试开关
    const DEBUG = true;
    // 浏览器UA
    const USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36';
    // 浏览器headers
    const headers = [
        'Accept-Language' => 'Accept-Language: zh-CN,zh;q=0.9',
        'Content-Type' => 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8'
    ];
    /**
     * 影视条目数据结构
     * @var array
     */
    const Subject = [
        'id' => 0,            // 电影条目id
        '@form' => '',        // douban、imdb
        'directors' => [],  // 导演
        'writers' => [],    // 编剧
        'casts' => [],      // 演员
        'title' => '',          // 中文标题
        'original_title' => '', // 原名
        'aka' => [],            // 又名
        'imdb' => '',           // IMDb编号
        'alt' => '',            // 电影条目页 URL
        'year' => '',           // 年份
        'rating' => [
            "max" => 10,
            "average" => 0,     // 评分
            "numRaters" => 0,   // 评分人数
            "min" => 0
        ],
        'images' => [
            'small' => '',
            'large' => '',
            'medium' => ''
        ],         // 电影海报图
        'subtype' => '',        // 条目分类
        'summary' => '',        // 剧情简介
        'pubdates' => [],       // 上映首映日期
        'mainland_pubdate' => '',   // 大陆上映日期
        'durations' => [],      // 片长
        'tags' => [],           // 标签
        'languages' => [],      // 语言
        'genres' => [],         // 影片类型
        'countries' => [],      // 制片国家/地区
        'videos' => [],     // 在线播放
        'photos' => [],     // 电影剧照
        'awards' => [],     // 获奖情况
        '@type' => '',      // 页面解码特有：条目类型
    ];
    /**
     * 演职员数据结构
     * @var array
     */
    const Celebrity = [
        'alt' => '',  // 个人页
        'id' => 0,   // ID
        'name' => '',  // 名字
        'avatar' => '',  // 头像
        'role' => '',  // 饰演角色
        'works' => [],  // 代表作
    ];
    // IMDb
    const IMDbBase = 'https://www.imdb.com';
    const IMDbUrl = array(
        'subject' => '/title/{id}/',                    // 电影条目页
        'celebrity' => '/title/{id}/fullcredits',       // 全部演职员
        'releaseinfo' => '/title/{id}/releaseinfo',     // 发行信息：各地域上映时间、别名等 Release Info
        'awards' => '/title/{id}/awards',               // 获奖信息
        'video' => '/title/{id}/videogallery',          // 所有视频
        'photos' => '/title/{id}/mediaindex',           // 所有照片 ?refine=poster 海报
        'keywords' => '/title/{id}/keywords',           // 关键字、标签
        'taglines' => '/title/{id}/taglines',           // 宣传语
        'summary' => '/title/{id}/plotsummary',         // 剧情简介
        'parentalguide' => '/title/{id}/parentalguide',     // 观影指南
        'companycredits' => '/title/{id}/companycredits',   // 发行公司信息
        'technical' => '/title/{id}/technical',         // 影片技术参数
        'locations' => '/title/{id}/locations',         // 拍摄地
        'name' => '/name/{id}/',            // 影人
        'company' => '/company/{id}/',      // 团体组织
        'works' => '',          // 影人作品
    );
    /**
     * 重要的正则表达式
     * @var array
     */
    const PrimaryRegex = array(
        'verify_tt_d' => '/^tt(\d+)$/i',
        'tt_d' => '/tt(\d+)/i',
        'tt' => '/tt\d+/i',
        'nm' => '/nm\d+/i',
        'co' => '/co\d+/i',
        'd' => '/\d+/i',
        'a' => '/<a .*?>.*?<\/a>/i',
        'duration' => '/^-?P(?=\w*\d)(?:\d+Y|Y)?(?:\d+M|M)?(?:\d+D|D)?(?:T(?:\d+H|H)?(?:\d+M|M)?(?:\d+(?:\\­.\d{1,2})?S|S)?)?$/i',      // PnYnMnDTnHnMnS
        'required' => '/placeholder/',
    );
    /**
     * UBB数据结构，编码模板
     */
    const ubbTemplet = [
        'images' => '[img]{}[/img]' . PHP_EOL,
        'title' => '◎译　　名　{}',
        'original_title' => '◎片　　名　{}',
        'year' => '◎年　　代　{}',
        'countries' => '◎产　　地　{}',
        'genres' => '◎类　　别　{}',
        'languages' => '◎语　　言　{}',
        'pubdates' => '◎上映日期　{}',
        'imdb_rating' => '◎IMDb评分　{}',
        'imdb' => '◎IMDb链接　{}',
        'douban_rating' => '◎豆瓣评分　{}',
        'alt' => '◎豆瓣链接　{}',
        'episodes_count' => '◎集　　数　{}',
        'durations' => '◎片　　长　{}',
        'directors' => '◎导　　演　{}',
        'writers' => '◎编　　剧　{}',
        'casts' => '◎演　　员　{}',
        'tags' => PHP_EOL . '◎标　　签　{}',
        'summary' => PHP_EOL . "◎简　　介\n　　{}",
        'awards' => PHP_EOL . "◎获奖情况　{}",
        'videos' => PHP_EOL . "◎在线播放\n{}",
    ];
    // 单例模式容器
    private static $Instance = [];
    // 错误消息
    protected $err_msg = '';

    // 文件缓存
    protected $IFileCache;
    // 文件缓存默认有效期7天：3600 * 24 * 7
    protected $cacheExpire = 604800;

    /**
     * 构造函数
     * @param string $cachePath
     */
    public final function __construct(string $cachePath = '')
    {
        $cachePath = empty($cachePath) ? APPLICATION_PATH . '/runtime/movie_cache' : $cachePath;
        $this->IFileCache = new IFileCache($cachePath, 'movie', false);
        $this->onInit();
    }

    /**
     * 初始化回调，子类可以重写此方法
     */
    protected function onInit()
    {
    }

    /**
     * 单例模式
     * @param string $className
     * @return Douban|IMDb
     * @throws BadRequestException
     */
    public static function getInstance(string $className = 'Douban')
    {
        // 映射表
        $classFileMap = [
            'douban' => 'Douban',
            'imdb' => 'IMDb',
        ];
        $site = strtolower($className);
        $className = $classFileMap[$site] ?? $className;

        if (!isset(self::$Instance[$site])) {
            $className = '\\Movie\\' . $className;
            if (class_exists($className)) {
                self::$Instance[$site] = new $className();
            } else {
                throw new BadRequestException($className . '类不存在');
            }
        }
        return self::$Instance[$site];
    }

    /**
     * 抽象方法，子类必须实现
     * @param $id
     * @return mixed
     */
    abstract public function SubjectPage($id);

    /**
     * 初始化方法，子类可重写此方法
     * @return $this
     */
    public function reset(): AbstractMovie
    {
        $this->err_msg = '';
        return $this;
    }

    /**
     * @return string
     */
    public function getError(): string
    {
        return $this->err_msg;
    }

    /**
     * 过滤trim、[\x00-\x1F]等特殊字符
     * @param mixed $raw
     * @return mixed
     */
    public static function filter($raw = '')
    {
        // 字符串
        if (is_string($raw)) {
            return preg_replace('/[\x00-\x1F]/', '', trim($raw));
        } // 数组
        elseif (is_array($raw)) {
            foreach ($raw as $k => &$v) {
                if (is_string($v)) {
                    $v = preg_replace('/[\x00-\x1F]/', '', trim($v));
                }
            }
            return $raw;
        } else {
            return $raw;
        }
    }

    /**
     * 将ISO 8601格式：PnYnMnDTnHnMnS，转换成秒
     * @param string $str
     * @return float|int|bool
     */
    public function ISO8601_to_seconds(string $str = 'PT1H59M')
    {
        try {
            $dv = new \DateInterval($str);
            return ($dv->y * 31536000) +
                ($dv->m * 2592000) +
                ($dv->d * 86400) +
                ($dv->h * 3600) +
                ($dv->i * 60) +
                $dv->s;
        } catch (\Exception $ex) {
            // 报错
            #todo...
            return false;
        }
    }

    /**
     * 获取网页的结构化数据
     * @param string $html
     * @return mixed|\SimpleXMLElement
     */
    public function getJsonLD(string $html = '')
    {
        $CDATA = Selector::select($html, '//script[@type="application/ld+json"]');
        $xml = simplexml_load_string("<xml><json>" . $CDATA . "</json></xml>", 'SimpleXMLElement', LIBXML_NOCDATA);
        // 数据清洗
        $result = preg_replace('/[\x00-\x1F]/', '', trim($xml->json));
        $rs = json_decode($result, true);
        if (empty($rs)) {
            /** json_last_error错误msg对照表：
             * 0 = JSON_ERROR_NONE
             * 1 = JSON_ERROR_DEPTH
             * 2 = JSON_ERROR_STATE_MISMATCH
             * 3 = JSON_ERROR_CTRL_CHAR
             * 4 = JSON_ERROR_SYNTAX
             * 5 = JSON_ERROR_UTF8
             */
            if (self::DEBUG) {
                die(__FUNCTION__ . '执行出错，错误码：json_last_error = ' . json_last_error());
            } else {
                return null;
            }
        }
        return $rs;
    }

    /**
     * curl请求
     * @param string $url 链接
     * @param mixed $fields 参数
     * @param bool $is_post 是否POST
     * @param bool $is_json 返回数据是否JSON
     * @return mixed
     */
    public function request(string $url, $fields = array(), bool $is_post = false, bool $is_json = false)
    {
        $cl = curl_init();

        if ($is_post) {
            curl_setopt($cl, CURLOPT_POST, true);
            if (!empty($fields)) {
                if (is_array($fields) || is_object($fields)) {
                    $skip = false;
                    foreach ($fields as $key => $value) {
                        // POST文件时（instanceof \CurlFile），跳过http_build_query
                        if ($value instanceof \CurlFile) {
                            $skip = true;
                        }
                    }
                    if (!$skip) {
                        $fields = http_build_query($fields);
                    }
                }
                curl_setopt($cl, CURLOPT_POSTFIELDS, $fields);
            }
        } else {
            curl_setopt($cl, CURLOPT_HTTPGET, true);
            if (!empty($fields)) {
                $url = $url . (strpos($url, '?') === false ? '?' : '&') . http_build_query($fields);
            }
        }

        curl_setopt($cl, CURLOPT_URL, $url);
        if (stripos($url, 'https://') === 0) {
            curl_setopt($cl, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($cl, CURLOPT_SSL_VERIFYHOST, 2);    // 可注释，可选值0,2
            curl_setopt($cl, CURLOPT_SSLVERSION, 1);        // 可注释
        }
        curl_setopt($cl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($cl, CURLOPT_USERAGENT, self::USERAGENT);
        curl_setopt($cl, CURLOPT_HTTPHEADER, array_values(self::headers));
        curl_setopt($cl, CURLOPT_CONNECTTIMEOUT, 30);
        curl_setopt($cl, CURLOPT_TIMEOUT, 60);
        $response = curl_exec($cl);
        $status = curl_getinfo($cl);
        curl_close($cl);
        if (isset($status['http_code']) && $status['http_code'] == 200) {
            if ($is_json) {
                $response = json_decode($response, true);
            }
            return $response;
        } else {
            if ($is_json) {
                return json_decode($response, true);
            }
            return null;
        }
    }

    /**
     * 获取缓存
     * @return mixed
     */
    public function getCache($key)
    {
        return null;
    }

    /**
     * 获取缓存key
     * @param array|string $key
     * @param string $delimiter 缓存分隔符   :实现Redis分组   _使用于文件缓存
     * @return string
     */
    public function getCacheKey($key, string $delimiter = ':'): string
    {
        return is_array($key) ? implode($delimiter, $key) : $key;
    }

    /**
     * 获取文件缓存
     * @param array|string $key
     * @return mixed
     */
    public function getFileCache($key)
    {
        $value = $this->IFileCache->get($this->getCacheKey($key, '_'));
        return $value ?: null;
    }

    /**
     * 设置文件缓存
     * @param array|string $key 缓存key
     * @param mixed $value 数据
     * @param int $expire 有效期
     * @return mixed
     */
    public function setFileCache($key, $value, int $expire = 1296000)
    {
        return $this->IFileCache->set($this->getCacheKey($key, '_'), $value, $expire);
    }

    /**
     * 电影条目信息，转换为UBB文本
     * @param array $data
     * @return array|bool
     */
    public function UBBFormat(array $data = [])
    {
        if (empty($data)) {
            return false;
        }
        $rs = ['format' => ''];
        $ds = ' / ';
        $form_site = !empty($data['@form']) ? $data['@form'] : '';
        $templet = self::ubbTemplet;
        // 遍历
        foreach ($templet as $key => $item) {
            // 前置预处理
            $is_key = isset($data[$key]) && $data[$key];
            $value = '';
            switch ($key) {
                case 'images':
                    $value = $data['images']['large'];
                    break;
                case 'title':
                    $value = is_array($data['aka']) && $data['aka'] ? $data['title'] . $ds . implode($ds, $data['aka']) : $data['title'];
                    break;
                case 'imdb':
                    $value = str_replace('{id}', $data[$key], self::IMDbBase . self::IMDbUrl['subject']);
                    break;
                case 'douban_rating':
                    if (($form_site === 'douban') || $is_key) {
                        $value = $this->getRatingAverage($data, $key);
                    }
                    break;
                case 'imdb_rating':
                    if ($form_site === 'imdb' || $is_key) {
                        $value = $this->getRatingAverage($data, $key);
                    }
                    break;
                case 'alt':
                    if ($form_site === 'douban') {
                        $value = $this->getSubjectDataByKey($key, $data, $ds);
                    }
                    break;
                case 'directors':
                case 'writers':
                case 'casts':
                    if ($is_key) {
                        $count = count($data[$key]) - 1;
                        foreach ($data[$key] as $k => $v) {
                            if ($k != 0) {
                                $value .= '　　　　　  ';
                            }
                            $value .= ($v['name'] ?? 'name null ') . ' (id:' . $v['id'] . ')';
                            $value .= ($count === $k) ? '' : PHP_EOL;
                        }
                    }
                    break;
                case 'awards':
                    if ($is_key) {
                        $count = count($data[$key]) - 1;
                        $val = 0;
                        foreach ($data[$key] as $k => $v) {
                            $value .= '　　' . $v['title'] . ' (' . $v['year'] . ')' . PHP_EOL;
                            if (isset($v['award']) && $v['award']) {
                                $val += count($v['award']);
                                foreach ($v['award'] as $vv) {
                                    $value .= '　　' . $vv['name'] . PHP_EOL;
                                    if (isset($vv['celebrity']) && $vv['celebrity']) {
                                        foreach ($vv['celebrity'] as $vvv) {
                                            $value .= '　　' . '　└──' . $vvv['name'] . ' (id:' . $vvv['id'] . ')' . PHP_EOL;
                                        }
                                    }
                                }
                            }
                            $value .= ($count === $k) ? '' : PHP_EOL;
                        }
                        $value = ($val ? '(共' . $val . '项)' : '') . PHP_EOL . $value;
                    }
                    break;
                case 'videos':
                    if ($is_key) {
                        $count = count($data[$key]) - 1;
                        foreach ($data[$key] as $k => $v) {
                            $url = $this->getVideoLink($v['video_id'], $v['source']['literal'], $v['sample_link']);
                            $value .= '　　' . $v['source']['name'] . ' ' . $url;
                            $value .= ($count === $k) ? '' : PHP_EOL;
                        }
                    }
                    break;
                default:
                    $value = $this->getSubjectDataByKey($key, $data, $ds);
                    break;
            }

            // 后置替换
            if ($value !== '') {
                $templet[$key] = str_replace('{}', $value, $templet[$key]);
            } else {
                unset($templet[$key]);
            }
        }
        $rs['format'] = implode(PHP_EOL, $templet);

        return $rs;
    }

    /**
     * 从条目数据中，挑选指定数据并拼接
     * @param string $key
     * @param array $data
     * @param string $ds
     * @return string
     */
    protected function getSubjectDataByKey(string $key = '', array $data = [], string $ds = ''): string
    {
        if (isset($data[$key]) && $data[$key]) {
            return is_array($data[$key]) ? implode($ds, $data[$key]) : $data[$key];
        }
        return '';
    }

    /**
     * 从条目数据中，拼接评分和评价人数
     * @param array $data
     * @param string $key
     * @return string
     */
    protected function getRatingAverage(array $data = [], string $key = 'rating'): string
    {
        // 1.特定站点评分不存在时，恢复默认的数据结构
        if (empty($data[$key]['average'])) {
            $key = 'rating';
        }
        if (isset($data[$key]['average']) && $data[$key]['average']) {
            return ($data[$key]['average'] + 0) > 0 ? $data[$key]['average'] . '/10 (' . ($data[$key]['numRaters'] ?? 0) . '人评价)' : '';
        }
        return '';
    }

    /**
     * 获取视频的在线播放URL
     * @param string $id // 视频ID
     * @param string $site // 视频站点
     * @param string $sample_link // 示例连接
     * @return string
     */
    public function getVideoLink(string $id = '', string $site = '', string $sample_link = ''): string
    {
        $url = '';
        switch ($site) {
            case 'iqiyi':       // 爱奇艺视频
                $url = str_replace('{id}', $id, 'https://www.iqiyi.com/v_{id}.html');
                break;
            case 'qq':          // 腾讯视频
                $url = str_replace('{id}', $id, 'https://v.qq.com/x/cover/{id}.html');
                break;
            case 'youku':       // 优酷视频
                $url = str_replace('{id}', $id, 'https://v.youku.com/v_show/id_{id}.html');
                break;
            case 'miguvideo':   // 咪咕视频
                $url = $sample_link;
                break;
            case 'bilibili':    // 哔哩哔哩
                $url = str_replace('{id}', $id, 'https://www.bilibili.com/bangumi/play/{id}');
                break;
            case 'xigua':       // 西瓜视频
                $url = str_replace('{id}', $id, 'https://www.ixigua.com/cinema/album/{id}');
                break;
            default:
                break;
        }
        return $url ?: $sample_link;
    }
}
